- 119 - ГЛАВА 5 (Андрэ Ла Мот) СЕКРЕТЫ VGA-КАРТ --------------------- Когда мы пишем видеоигры, то используем компьютер для создпния миров и VGA-карту для рисования разных объектов. В этом случае VGA-карта является "связующей нитью" между компьютером и экраном дисплея. В данной главе мы поговорим о следующем: - Обзор VGA-карт; - 256-цветный режим; - Конфигурация видеопамяти; - Таблицы цветов; - Переопределение цветовой палитры; - Цветовое вращение; - Рисование точек; - Рисование линий; - Файлы РСХ; - Вывод битовых образов (бит-блиттинг); - Спрайты; - Вывод текста; - Дублирующее буферизирование; - Синхронизация с вертикальной разверткой; - Игра Tombstone. - 120 - ОБЗОР VGA-КАРТ VGA-карта - это плата, которая генерирует видеосигнал, передаваемый на дисплей. Этот видеосигнал состоит из серии импульсов, определяющих цвет и интенсивность каждой точки на экране. Не будем вдаваться в подробности о том, как он образуется или как карта VGA создает все временные задержки для корректной работы с дисплеем. У нас есть единственный вопрос: как мы можем поместить пиксель нужного цвета на экран? Нарисовать одиночную точку на экране было бы достаточно непростой задачей, и нам пришлось бы проделать для этого множество разных вещей. Но, проделав это один раз, мы сможем в дальнейшем вызывать уже написанные функции и никогда больше об этом не вспоминать. Как вы знаете, VGA-карта поддерживает много различных графических режимов и может быть перепрограммирована для подучения совершенно фантастических разрешений (например, я создал режим 320х400, 256 цветов путем прямого перепрограммирования временных регистров VGA-карты). Такая функциональность стала возможной благодаря архитектуре VGA-карты. Карта VGA имеет ряд регистров, которые можно по-разному программировать для решения различных задач. В этом случае программирование регистров VGA - это тема для отдельной книги. Если вы этим огорчены - не отчаивайтесь, существует множество книг на эту тему. В моей маленькой домашней библиоеке более 30 книг посвящены VGA-картам. Если вы хотите узнать больше про секреты VGA, то я уверен, у вас не возникнет проблем с источниками информации. Один из путей приблизиться к секретам VGA-карты, это рассматривать ее как систему, поддерживающую только один режим; например, режим, который мы и будем применять при создании наших игр, дающий разрешение 320х200 точек, 256 цветов, называемый режимом 13h. Детали мы обсудим позже в этой главе. Используя только один из всевозможных графических режимов, мы становимся более независимыми от различий карт VGA. Почему? Наши игры будут работать только на VGA и SVGA и нам не потребуется поддерживать EGA, монохромные и другие карты. Часто разработчики видеоигр пишут десятки драйверов для поддержки разных карт и мониторов. Если мы сосредоточимся на одном режиме, то сможем освободиться от излишней общности и будем писать код только один раз. Обычно, когда вы пишете программу, то стараетесь сделать ее достаточно универсальной. К сожалению, это не относится к видеоиграм для ПК. Игровые программы настолько насыщены графикой и вычислениями, что нам приходится все переписывать заново для каждой новой игры. Ведь мы не делаем текстовых процессоров, а создаем новые миры, и не можем повторяться, используя библиотеки от старых игр. Я думаю, это правильно. - 121 - Мы будем использовать VGA в режиме 13h. Но как этот режим работает? Прочитав следующий раздел, вы найдете ответ на этот вопрос. 256-ЦВЕТНЫЙ РЕЖИМ В режиме 13h (или 19 в десятичной нотации) экран состоит из 320 пикселей по горизонтали и 200 - по вертикали. В нем поддерживается одновременное отображение до 256 цветов. Теперь мы приближаемся к пониманию истинной ценности режима 13h. Он является наиболее простым в программировании. Как вы заметили, все графические режимы (CGA, EGA, VGA, SVGA, XGA и другие) используют некоторую область памяти для представления битовой карты, выводимой на экран. Этот раздел памяти называется видеобуфером. Для режима 13h и VGA-карт видеобуфер начинается с адреса A000:0000 и простирается до адреса A000:F9FF. Если вы владеете шестнадцатеричной арифметикой, то легко увидите, что размер буфера равен 64000 байт. Умножив 320 на 200, мы получим тот же результат. Это значит, что каждый пиксель в режиме 13h представлен одним байтом. Этот замечательный факт заставляет меня любить VGA-карту. На рис.5.1 показано строение видеобуфера VGA. Рис.5.1. Видеобуфер VGA. - 122 - Позже в этой главе мы узнаем, как адресовать видеобуфер и записывать в него пиксели. Сейчас же я хочу поговорить о другом. Может возникнуть вопрос: "А не является ли экран 320х200 слишком маленьким?" Ответом будет: "И да, и нет." Сегодня разрешение 320х200 устарело, но благодаря 256 цветам и правильному рендерингу оно и по сю пору выглядит восхитительно. Эти плюсы заставляют нас забыть о низком разрешении. Кстати, игрок никогда не должен даже подозревать, что программа работает в режиме с низкой разрешающей способностью экрана. КОНФИГУРАЦИЯ ВИДЕОПАМЯТИ Давайте поговорим о записи в видеобуфер. Как вы можете видеть из рис.5.1, видеобуфер начинается с адреса A000:0000 и заканчивается по адресу A000:FF9F. Поскольку каждый пиксель представлен одним байтом, то адресация видеобуфера довольно проста.Все, что нам нужно сделать, это определить указатель на видеобуфер, после чего можно записывать пиксели, используя этот указатель. И все! Видеобуфер - это одна большая, непрерывная область памяти или массив байтов. Мы знаем, что он состоит из 320 байтов в каждой строке и таких строк насчитывается 200. Таким образом, чтобы нарисовать пиксель с координатами (x,y), нам надо: - Умножить координату Y на 320; - Прибавить X-координату к результату умножения; - Использовать рассчитанное значение как смещение от базового указателя на видеобуфер; - По полученному адресу записать значение от 0 до 255. ТАБЛИЦЫ ЦВЕТОВ Как я уже говорил, мы можем записать по рассчитанному адресу число от 0 до 255. А что означает это число? Ответ прост - это цвет пикселя, который мы хотим отобразить. VGA-карта способна одновременно отобразить на экране до 256 цветов. Цвет, который мы хотим получить, должен быть представлен числом от 0 до 255. Это здорово, но какая связь между числом и действительным цветом? Число используется как индекс в таблице цветов, хранящей действительные значения цвета, который мы увидим на экране. Всего же VGA-карта способна отобразить 262144 цвета. Таким образом, если нам надо записать в видеобуфер значение цвета, то нам понадобится три байта для представления числа такой длины. Впрочем, и видеобуфер в этом - 123 - случае будет просто огромен. Поэтому создатели карты предусмотрели возможность переадресации графического адаптера. Переадресация означает, что одна числовая величина используется в качестве адреса другогоьзначения (примерно, как именованный указатель в Си). Вместо одновременного воспроизведения всех 262144 цветов, разработчики VGA-карт дали возможность использовать подмножество из 256 цветов. В результате VGA-карта имеет таблицу отображения цветов, включающую 256 значений. Каждое из этих значений состоит из 256 элементов размером в один байт, содержащих значения красного, синего и зеленого для выбранного цвета (помните, красный, зеленый и синий в комбинации могут образовывать любой цвет.) Таблица цветов состоит из 768 байт (3х256). Например, когда карта считывает из видеобуфера число 72, то она проверяет адрес 72 в таблице цветов. Адрес 72 находится по смещению 72х3 от начала таблицы, поскольку каждое из значений занимает 3 байта. Затем значения зеленого, красного и синего считываются из таблицы и используются как значения сигналов. Давайте рассмотрим рис.5.2 для более подробного ознакомления. Рис.5.2. Выбор составляющих цвета из таблицы. - 124 - Было бы прекрасно, если б мы имели прямой доступ к таблице соответствия, как к обычной памяти. Но, к сожалению, регистры цвета доступны только через порты ввода-вывода VGA-карты. На самом деле это плохо, поскольку весьма усложняет нам жизнь. Теперь нам предстоит узнать, как менять значения в таблице цветов. ПЕРЕОПРЕДЕЛЕНИЕ ЦВЕТОВОЙ ПАЛИТРЫ Таблица цветов организована в VGA-карте как регистровый файл. (Я использовал слово регистр, чтобы обозначить значение в таблице соответствия. Каждый регистр палитры - это 24 бита.) Для доступа к значению мы должны произвести некоторые действия. Мы не можем просто сказать: "Изменить компонент красного для значения 123". Мы должны модифицировать все три составляющих цвета, который хотим изменить. Хотя каждое значение состоит из 3 байтов (один для каждой из составляющих), только первые 6 битов каждого байта используются для обозначения цвета. Существует 64 оттенка для каждого цвета, или 2 в 18-й степени различных цветов (это и есть общее количество цветов - 262144). Таким образом, если вы поместите значение, превышающее размер в 6 битов (или 63), то можете нарушить все компоненты, но не изменить цвет. Итак, нам требуется только три порта ввода-вывода для решения задачи изменения значений в таблице соответствия цветов. #define PALETTE_MASK 0x3C6 #define PALETTE_REGISTER_RD 0x3C7 #define PALETTE_REGISTER_WR 0x3C8 #define PALETTE_DATA 0x3C9 Теперь посмотрим, как это реализуется: - Порт 0х3С6 называется маской палитры и используется для маскирования битов нужного регистра палитры. Например, если вы поместите в этот порт число 0х00, то получите регистр 0, независимо от того, какой регистр запрашиваете. С другой стороны, если вы запишете в регистр маски значение 0xFF, то получите возможность доступа к любому регистру через индекс регистра палитры 0x3C8 и 0x3C7 (первый из них используется для записи, а второй - для чтения); - Порт 0x3C7, называемый регистром чтения палитры, используется для выбора из таблицы цветов значения, которое вы хотите прочитать; - Порт 0x3C8 называется регистром записи палитры и используется для выбора в таблице соответствия значения, которое вы хотите записать; - Наконец, данные красной, зеленой и синей составляющей вы можете записать или прочитать из порта по адресу 0x3C9, называемого портом данных палитры. - 125 - Вы можете спросить: "А как мы прочитаем из одного порта три байта?" На самом деле вы можете прочитать их по очереди. После того, как вы выберете необходимый регистр (значение таблицы цветов, к которому вам нужен доступ), то первый записанный в регистр палитры байт будет соответствовать значению красного цвета. Второй байт задаст значение зеленого цвета, ну а третий - синего. Когда вы будете читать, это правило будет также верно, но в отличие от записи трех байтов в каждый момент чтения вы будете получать следующий компонент считываемого значения выбранного регистра. Для записи в регистр палитры вы должны: - Выбрать регистр, который хотите изменить; - Произвести три записи в порт регистра данных. Когда вы читаете или пишете в регистр цвета, не забывайте каждый раз предварительно изменять значение маски на 0xFF. Листинг 5.1 показывает код, содержащий эти шаги. Листинг 5.1. Запись в регистр палитры. ------------------------------------------------------------------------- void Set_Palette_Register(int index, RGB_color_ptr color) { // эта функция устанавливает один из элементов таблицы цветов. // номер регистра задается параметром index, цвет - структурой color // указываем, что мы будем обновлять регистр палитры _outp(PALETTE_MASK,0xff); // какой из регистров мы хотим обновить _outp(PALETTE_REGISTER_WR, index); // теперь обновляем RGB. Обратите внимание, // что каждый раз используется один и тот же порт _outp(PALETTE_DATA,color->red); _outp(PALETTE_DATA,color->green); _outp(PALETTE_DATA,color->blue); } // конец функции ________________________________________________________________________ Помните, что я использую структуру данных RGB_color. Это структура с тремя полями, каждое из которых предназначено для определенного цвета. Она выглядит так: - 126 - typedef struct RGB_color_typ { unsigned char red; // красный компонент 0-63 unsigned char green; // зеленый компонент 0-63 unsigned char blue; // синий компонент 0-63 } RGB_color, *RGB_color_ptr; Все походит на то, что следующей операцией должно стать чтение из регистра. Мы сделаем то же самое, что и в Set_Palette_Register, только вместо записи в порт палитры будем читать из него и пересылать полученные значения в структуру RGB_color. Листинг 5.2 содержит необходимый для этого код. Листинг 5.2. Чтение регистра палитры. ------------------------------------------------------------------------- void Get_Palette_Register(int index, RGB_color_ptr color) { // функция читает регистры цвета и помещает полученные значения // в поля структуры color // установить маску регистра палитры _outp(PALETTE_MASK,0xff); // читаем данные color->red = _inp(PALETTE_DATA); color->green = _inp(PALETTE_DATA); color->blue = _inp(PALETTE_DATA); } // конец функции __________________________________________________________________________ Теперь, когда мы знаем, как читать и писать в регистр палитры, почему бы нам не создать функцию для получения новой цветовой палитры? Неплохая идея! Напишем для этого функцию, которая строит палитру, имеющую 64 оттенка серого, красного и голубого. Листинг 5.3 содержит ее код. Листинг 5.3. Создание новой цветовой палитры. ------------------------------------------------------------------------- void Greate_Cool_Palette(void) { // эта функция создает новую палитру, содержащую по 64 оттенка // серого, красного, зеленого и синего цветов - 127 - RGB_color color; int index; // в цикле последовательно создаем цвета и меняем значения регистров for (index=0; index < 64; index++) { // это оттенки серого color.red = index; color.green = index; color.blue = index; Set_Palette_Register(index, (RGB_color_ptr)&color); // это оттенки красного color.red = index; color.green = 0; color.blue = 0; Set_Palette_Register(index+64, (RGB_color_ptr)&color); // это оттенки зеленого color.red = 0; color.green = index; color.blue = 0; Set_Palette_Register(index+128, (RGB_color_ptr)&color); // это оттенки синего color.red = 0; color.green = 0; color.blue = index; Set_Palette_Register(index+192, (RGB_color_ptr)&color); } // конец цикла for } // конец функции _________________________________________________________________________ Наличие возможности изменения цветовой палитры позволяет нам создавать различные интересные эффекты освещения и анимации в наших играх. (Например, как сделаны цветовые эффекты в DOOM`е? Часть цветовой палитры изменяется "на лету" во время игры.) Так достигаются эффекты освещения и стельбы. Это особенно хорошо для реализации эффекта "отключения света". Вы должны принять на веру работоспособность функции из Листинга 5.3, поскольку у нас нет возможности проверить ее. Прежде мы должны были бы разработать эффективные функции для рисования точки и линии, но сейчас я хочу поговорить о технике, применение которой мы вскоре найдем. - 128 - ЦВЕТОВАЯ РОТАЦИЯ Когда я купил свой первый компьютер в 1978 г. (хотя я до сих пор уверен, что это был божий дар), я был глубоко поражен компьютерной графикой и видеоиграми. Компьютер назывался Atari 800. В то время это было пределом возможностей. Один из интересных эффектов, который он поддерживал, назывался цветовой ротацией. Цветовая ротация может быть выполнена только на компьютерах, имеющих таблицу преобразования цветов. Как известно, изображение рисуется на экране компьютера. Например, пусть это будет водопад. Изображение водопада состоит (в нашем случае) из 16 оттенков синего цвета, каждый из которых - это число, ссылающееся в таблице преобразования на соответствующее значение цвета. Мы получаем, что водопад содержит 16 оттенков синего, находящихся в регистрах цвета с адресами от 100 до 115. Теперь представьте, что мы берем одно из значений и сдвигаем его в следующий регистр и так далее до 115-го, содержимое которого переносим в 100-й регистр. Что произойдет? Правильно, возникает ощущение движения. Уже в 70-х годах это было возможно на процессорах 6502 с тактовой частотой 1.79 МГц. Мы применим эту технику позже. Есть куча классных вещей, которые можно сделать, используя этот прием, тем более, что в ПК сейчас целых 256 регистров. А пока постарайтесь просто запомнить этот трюк. РИСОВАНИЕ ТОЧКИ Однажды я сказал: "Дайте мне адрес видеобуфера, и я переверну экран...". Это весьма правдивое высказывание. Во всех системах с отображением адресов видеопамяти на область адресов памяти обычной, как это делается в ПК, рендеринг был бы более простой и доступной вещью, если бы видеобуфер имел хоть каплю логики в своей организации. Собственно, организация и логика есть: видеобуфер - это один большой массив. И все. Как мы узнали чуть раньше, для рисования точки нам достаточно вычислить адрес смещения относительно начала видеобуфера (A000:0000) и записать байт, отображающий ее цвет. Больше ничего делать не надо. Листинг 5.4 содержит фрагмент кода, который рисует точку определенного цвета с координатами X и Y. Листинг 5.4. Рисование точки в позиции (x,y). -------------------------------------------------------------------------- void Plot_Pixel(int x, int y, unsigned char color) { // эта функция отображает точку выбранного цвета. Каждая строка // занимает 320 байт, поэтому для вычисления адреса надо умножить Y // на 320 и прибавить значение X video_buffer[y*320+x]=color; } // конец функции __________________________________________________________________________ - 129 - Итак, рисование точки довольно просто. Я думал, все окажется сложнее, и поэтому так много написал об этом, но то, что мы использовали режим 13h, значительно упростило дело. Функция Plot_Pixel получилась простой, это всего одна строка кода. Тем не менее, давайте попробуем ее оптимизировать. В книге есть целая глава, посвященная оптимизации, но эта единственная строка, содержащая операцию умножения, сводит меня с ума. Давайте избавимся от умножения. Кстати, возьмем за правило избегать операций умножения и вообще откажемся от действий с плавающей запятой. Итак, посмотрим, что мы можем сделать с вычислением y x320? Вспомним, что мы используем двоичную арифметику и все числа в ПК также представлены в двоичном виде. Если вы берете двоичное число и сдвигаете его влево или вправо, Это аналогично его умножению или делению на два. Рис.5.3 поясняет это. Рис.5.3. Битовый сдвиг для реализации умножения и деления. Поскольку операция выполняется примерно в 2-10 раз быстрее, чем умножение, то мы получим быстрые функции для рисования. Единственная сложность состоит в том, что число 320 - это не степень двух, но чтобы выйти из положения, мы применим маленькую хитрость. Представим выражение 320 х y как 256 x y + 64 x y. Листинг 5.5 показывает код для быстрого рисования точки. Листинг 5.5. Программа быстрого рисования точки. --------------------------------------------------------------------------- void Plot_Pixel_Fast ( int x, int y, unsigned char color ) { // эта функция рисует точку несколько быстрее за счет замены // операции умножения нас сдвиг. - 130 - // учитываем, что 320*y=256*y+64*y=y<<8+y<<6 video_buffer[ ( (y<<8) + (y<<6) ) + x ] = color; } // конец функции __________________________________________________________________________ Эта функция работает примерно в два раза быстрее - вот что значит оптимизация. Позже мы научимся оптимизировать программы так, что парни из Microsoft перестанут нам верить. Теперь, когда у нас есть функция рисования точки, надо дополнить нашу графическую библиотеку функцией рисования линии. РИСОВАНИЕ ЛИНИЙ Нас совершенно не интересует написание функций для рисования линий с применением сложностей типа алгоритма Брезинхема. Более того, в наших играх вообще не будет линий, проходящих под произвольным углом. Напротив, у нас будем множество вертикальных линий и, может быть, немного горизонтальных. На самом деле, большинство игр вообще обходится без рисования линий, так как в них используются битовые массивы. Мы напишем две функции. Одна из них рисует горизонтальные линии слева направо, а другая - вертикальные сверху вниз. Рис.5.4 показывает, как они выглядят в видеобуфере. Поскольку вертикальные линии рисовать легче, то с них мы и начнем. Как видно из рис.5.4, горизонтальную линию можно получить, заполняя ряд пикселей в матрице 320х200. Чтобы это проделать, мы должны найти начальный адрес строки и заполнить ее значениями пикселей от начальной до конечной позиции. Для этого стоит использовать функцию memset. Это один из самых быстрых способов. Листинг 5.6 содержит код такой функции. Листинг 5.6. Рисование горизонтальной линии. -------------------------------------------------------------------------- void H_Line (int x1, int x2, int y, unsigned int color) { // функция рисует горизонтальную линию, используя memset() // x2 должно быть больше x1 _fmemset((char far *)(video_buffer + ((y << 8) + (y << 6)) + x1), color, x2 - x1 + 1); } // конец функции __________________________________________________________________________ Следует кое-что запомнить: - Мы используем функцию _fmtmset, поскольку она, в отличие от memset, корректно работает с дальними (FAR) указателями; - 131 - Рис.5.4. Видеобуфер VGA. - Мы вычисляем начальный адрес линии, и функция заполняет определенное количество байтов заданным значением цвета; - Количество байтов равно длине строки, которую мы вычисляем как разность между правым и левым концами линии. При этом нужно прибавить единицу, чтобы не потерять последнюю точку. К сожалению, мы не можем использовать семейство функций memset для рисования вертикальных линий, поскольку они работают только с непрерывными областями памяти. В случае вертикальных линий каждый следующий пиксель отстоит на 320 байт от предыдущего. Если вы находитесь в какой-то точке и хотите рисовать линию вниз, то вам надо прибавлять 320, а если линия рисует вверх, то надо вычитать 320 из текущего адреса. Таким образом, мы можем создать цикл, который увеличивает адрес на 320 и рисует вертикальную линийю в виде пикселей сверху вниз. Листинг 5.7 демонстрирует код этой функции. Листинг 5.7. Рисование вертикальной линии. --------------------------------------------------------------------------- void V_Line(int y1, int y2, int x,unsigned int color) { // рисуем вертикальную линию (y2 больше y1) - 132 - unsigned int line_offset, index; // вычисляем начальную позицию line_offset = ((y1<<8) + (y1<<6)) + x; for (index=0; index<=y2-y1; index++) { video_buffer[line_offset] = color; line_offset+=320; // переходим к следующей линии } // конец цикла for } // конец функции __________________________________________________________________________ Функция V_Line несколько длиннее H_Line, поскольку она сама производит все адресные вычисления. Эта функция фантастически эффективна - вспомните пример использования сдвига вместо умножения (кстати, если вы до сих пор не поняли смысл двоичного сдвига, не отчаивайтесь - в 18-й главе "Техника оптимизации" мы это подробно изучим). Прежде, чем перейти к следующей теме, я хочу дать вам замечательную программу, которая создает новую палитру и выводит ее на экран, используя функции рисования вертикальных линий. В ней встречается уже известная функция Set_Mode(), описанная во 2-й главе, поэтому здесь я не включил ее исходный код. При желании вы можете взять его из 2-ой главы. Более того, эта функция объявлена в программе как EXTERNAL, так что ее можно просто прилинковать. Листинг 5.8 содержит необходимый код программы Show_Palette. Примечание ------------- Я обнаружил некоторую проблему с чтением регистра палитры на VGA-картах. Похоже, что вы не всегда можете получить доступ к требуемому вам регистру. Это ошибка "железа", и она характерна для отдельных VGA-карт. Я решил эту проблему довольно примитивным способом: просто дважды читаю каждый регистр. Вроде, мне это помогло. В своих программах вы можете попробовать включать дополнительные проверки и использовать разные функции для чтения регистров палитры в случае обнаружения ошибки. Во всяком случае, время, потраченное на решение данной задачи с лихвой окупится в процессе выполнения программы. --------------------- - 133 - Листинг 5.8. Создание и отображение цветовой палитры (PALDEMO.C). -------------------------------------------------------------------------- // I N C L U D E S /////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include // D E F I N E S //////////////////////////////////////////////////////////// #define ROM_CHAR_SET_SEG 0xF000 // segment of 8x8 ROM character set #define ROM_CHAR_SET_OFF 0xFA6E // begining offset of 8x8 ROM character set #define VGA256 0x13 #define TEXT_MODE 0x03 #define PALETTE_MASK 0x3c6 #define PALETTE_REGISTER_RD 0x3c7 #define PALETTE_REGISTER_WR 0x3c8 #define PALETTE_DATA 0x3c9 #define SCREEN_WIDTH (unsigned int)320 #define SCREEN_HEIGHT (unsigned int)200 // S T R U C T U R E S /////////////////////////////////////////////////////// // this structure holds a RGB triple in three bytes typedef struct RGB_color_typ { unsigned char red; // red component of color 0-63 unsigned char green; // green component of color 0-63 unsigned char blue; // blue component of color 0-63 } RGB_color, *RGB_color_ptr; // E X T E R N A L S ///////////////////////////////////////////////////////// extern Set_Mode(int mode); // P R O T O T Y P E S /////////////////////////////////////////////////////// void Set_Palette_Register(int index, RGB_color_ptr color); void Get_Palette_Register(int index, RGB_color_ptr color); void Create_Cool_Palette(); void V_Line(int y1,int y2,int x,unsigned int color); // G L O B A L S //////////////////////////////////////////////////////////// unsigned char far *video_buffer = (char far *)0xA0000000L; // vram byte ptr unsigned int far *video_buffer_w= (int far *)0xA0000000L; // vram word ptr // F U N C T I O N S ///////////////////////////////////////////////////////// void Set_Palette_Register(int index, RGB_color_ptr color) { // this function sets a single color look up table value indexed by index // with the value in the color structure // tell VGA card we are going to update a pallete register _outp(PALETTE_MASK,0xff); // tell vga card which register we will be updating _outp(PALETTE_REGISTER_WR, index); // now update the RGB triple, note the same port is used each time _outp(PALETTE_DATA,color->red); _outp(PALETTE_DATA,color->green); _outp(PALETTE_DATA,color->blue); } // end Set_Palette_Color /////////////////////////////////////////////////////////////////////////////// void Get_Palette_Register(int index, RGB_color_ptr color) { // this function gets the data out of a color lookup regsiter and places it // into color // set the palette mask register _outp(PALETTE_MASK,0xff); // tell vga card which register we will be reading _outp(PALETTE_REGISTER_RD, index); // now extract the data color->red = _inp(PALETTE_DATA); color->green = _inp(PALETTE_DATA); color->blue = _inp(PALETTE_DATA); } // end Get_Palette_Color ////////////////////////////////////////////////////////////////////////////// void Create_Cool_Palette(void) { // this function creates a cool palette. 64 shades of gray, 64 of red, // 64 of green and finally 64 of blue. RGB_color color; int index; // swip thru the color registers and create 4 banks of 64 colors for (index=0; index < 64; index++) { // grays color.red = index; color.green = index; color.blue = index; Set_Palette_Register(index, (RGB_color_ptr)&color); // reds color.red = index; color.green = 0; color.blue = 0; Set_Palette_Register(index+64, (RGB_color_ptr)&color); // greens color.red = 0; color.green = index; color.blue = 0; Set_Palette_Register(index+128, (RGB_color_ptr)&color); // blues color.red = 0; color.green = 0; color.blue = index; Set_Palette_Register(index+192, (RGB_color_ptr)&color); } // end index } // end Create_Cool_Palette ////////////////////////////////////////////////////////////////////////////// void V_Line(int y1,int y2,int x,unsigned int color) { // draw a vertical line, note y2 > y1 unsigned int line_offset, index; // compute starting position line_offset = ((y1<<8) + (y1<<6)) + x; for (index=0; index<=y2-y1; index++) { video_buffer[line_offset] = color; line_offset+=320; // move to next line } // end for index } // end V_Line //M A I N ///////////////////////////////////////////////////////////////////// void main(void) { int index; RGB_color color,color_1; // set video mode to 320x200 256 color mode Set_Mode(VGA256); // create the color palette Create_Cool_Palette(); // draw a bunch of vertical lines, one for each color for (index=0; index<320; index++) V_Line(0,199,index,index); // wait for user to hit a key while(!kbhit()) { Get_Palette_Register(0,(RGB_color_ptr)&color_1); Get_Palette_Register(0,(RGB_color_ptr)&color_1); for (index=0; index<=254; index++) { Get_Palette_Register(index+1,(RGB_color_ptr)&color); Get_Palette_Register(index+1,(RGB_color_ptr)&color); Set_Palette_Register(index,(RGB_color_ptr)&color); } // end for Set_Palette_Register(255,(RGB_color_ptr)&color_1); } // end while // go back to text mode Set_Mode(TEXT_MODE); } // end main ___________________________________________________________________________ Программа из Листинга 5.8 создает новую палитру, которая содержит 64 оттенка всех основных цветов, включая серый. затем она разделяет каждый цвет вертикальными линиями и после этого перемешивает их. Вроде, хватит об этом. Теперь стоит поговорить о том, как целиком прочитать файл с образом. Начнем с формата PCX-файлов. ГРАФИЧЕСКИЙ ФОРМАТ PCX В индустрии компьютерной графики существует так много стандартов, что само слово "стандарт" уже потеряло свой первоначальный смысл. Сегодня существует несколько наиболее известных стандартов: PCX, GIF, RGB, TGA, TIF и многие другие. Нам интересен формат PCX потому, что сегодня он является самым распространенным. Файл в формате PCX представляет собой закодированное представление изображения. Кодирование необходимо для уменьшения размера файла, поскольку только один образ 320х200 пикселей уже займет 64К памяти. Рисованные объекты обладают большой цветовой избыточностью, и это обстоятельство используется для сжатия изображения. - 138 - Рис.5.5. Экран для игры Warlock. Примечание ----------- Отсканированные фотографии, как правило, содержат большое количество цветов, более илди менее равномерно распределенных по всему изображению. Таким образом, фотографические изображения с трудом поддаются сжатию. Однако, поскольку большинство картинок в компьютерных играх выполняется вручную, мы можем не особенно ломать голову над проблемами, возникающими при работе с оцифрованными фотографиями. ------------------ Давайте посмотрим на рис.5.5 (файл CH19\WARINTR2.PCX на дискете). Это копия экрана из игры Warlock. Как вы можете заметить, там не слишком много цветов. Более того, на нем присутствует множество больших, одинаково окрашенных областей. Как правило, в экранных изображениях используется лишь ограниченное число цветов. Так почему бы не подсчитать количество пикселей одинакового цвета и не сохранить их как целые группы, вместе с позицией и цветом. Это можно сделать. В общем, подобная технология, правда, в несколько усовершенствованном виде, и применена в PCX-формате. Для сжатия информации этот формат использует так называемое RLE-кодирование. При этом изображение обрабатывается построчно без учета расположения пикселей по вертикали. Посмотрите ра рис.5.6. Преобразование экранного файла в PCX-формат происходит следующим образом: просматривается очередная строка изображения, и если это возможно, сжимается. Сжатие выполняется в том случае, если найдена последовательность пикселей одинакового цвета. При этом сохраняется - 139 - Рис.5.6. Схема сжатия методом RLE. количество повторений пикселей и значение их цвета. Повторяется это до тех пор, пока все изображение не окажется сжатым. Такая методика неплохо работает для большинства изображений, но в некоторых случаях может не срабатывать, например, когда изображение имеет слишком мало одинаковых участков. В этом случае размер файла, напротив, увеличится, так как для записи одиночных пикселей требуется не один, а два байта. Файл формата PCX состоит из трех секций: - Первая секция PCX-файла длиной 128 байт содержит различную служебную информацию; - Вторая секция - это данные сжатого образа, которые могут оказаться любой длины; - Третья секция размером в 768 байт содержит цветовую палитру, если она есть. В нашем случае она будет присутствовать, поскольку мы используем 256-цветный режим 13h. Эти 768 байт хранят значения RGB от 0 до 255. Суммируя вышесказанное, можно нарисовать структуру PCX-файла (рис.5.7). Получение информации из заголовка несложно: достаточно прочитать первые 128 байт и отформатировать их в соответствии со структурой, представленной в Листинге 5.9. - 140 - Рис.5.7. Формат PCX. Листинг 5.9. Структура заголовка PCX-файла. -------------------------------------------------------------------------- tyredef struct pcx_header_typ { char manufacturer; // всегда 10 char version; // 0 - версия 2.5 Paintbrush // 2 - версия 2.8 с палитрой // 3 - версия 2.8 без палитры // 5 - версия 3.0 или старше char encoding; // всегда 1 - RLE кодирование char bits_per_pixel; // количество бит на пиксель // для нашего случая - 8 int x,y; // координаты верхнего левого угла изображения int width,height; // размеры изображения int horz_res; // количество пикселей по горизонтали int vert_res; // количество пикселей по вертикали char ega_palette[48]; // EGA-палитра. Ее можно игнорировать char reserved; // ничего значимого char num_color_planes; // количество цветовых плоскостей // в изображении int bytes_per_line; // количество байт на одну строку int palette_type; // не беспокойтесь об этом char padding[58]; // ссылка на палитру в конце файла } pcx_header, *pcx_header_ptr; ___________________________________________________________________ Последнюю часть PCX-файла также довольно легко обработать: - Необходимо установить указатель на конец файла; - Передвинуться вверх на 768 байт; - Прочитать 768 байт как палитру. Конечно, я упустил кое-какие детали обработки PCX-файла, но сделал это лишь для того, чтобы лучше передать смысл производимых действий. Сейчас же нас больше должен занимать способ декодирования средней части, где находится само изображение. Именно отсюда начинаются сложности, поскольку процедура декомпрессии не очень проста и очевидна. - Если код прочитанного байта принадлежит множеству 192...255, то мы вычитаем из него 192 и используем полученный результат, как количество повторений следующего байта; - Если код прочитанного байта лежит в диапазоне от 0 до 191, то мы используем его как байт данных, то есть помещаем его в битовую карту без изменений. Если вы достаточно внимательны, то можете спросить: "А как же быть с пикселями, имеющими значения от 192 до 255? Интерпретируются ли они как RLE-цепочки?" Да, и гениальное решение этого вопроса состоит в том, что такие значения кодируются не одним, а двумя байтами. Например, если требуется Рис.5.8. Декомпрессия PCX-файла. - 142 - поместить в файл значение 200, то сначала нужно записать число 193 (192-1) как количество повторений, а потом - 200. Посмотрим на рис.5.8, чтобы увидеть пример декомпрессии. Теперь настало время написать программу, реализующую чтение файла формата PCX. Она получилась весьма неплохой. Листинг 5.10 дает вам возможность убедиться в этом самостоятельно. Листинг 5.10. Программа чтения файла формата PCX. -------------------------------------------------------------------------- // размеры экрана #define SCREEN_WIDTH 320 #define SCREEN_HEIGHT 200 // структура для хранения данных PCX файла typedef struct pcx_picture_typ { pcx_header header; // заголовок файла (длина 128 байт) RGB_color palette[256]; // палитра char far *buffer; // буфер для размещения изображения // после декомпрессии } pcx_picture, *pcx_picture_ptr; void PCX_Load(char *filename, pcx_picture_ptr image,int enable_palette) { // функция загружает данные из PCX-файла в структуру pcx_picture // после декомпрессии байты изображения помещаются в буфер. // Отдельные элементы изображения выделяются позднее. Также // загружается палитра и заголовок FILE *fp; int num_butes,index; long count; unsigned char data; char far *temp_buffer; // открыть файл fp = fopen(filename,"rb"); // загрузить заголовок temp_buffer = (char far *)image; for (index=0; index<128; index++) { temp_buffer[index] = getc(fp); } // конец цикла for - 143 - // загрузить данные и декодировать их в буфере count=0; while(count<=SCREEN_WIDTH * SCREEN_HEIGHT) { // получить первую часть данных data = getc(fp); // это RLE? if (data>=192 && data<=255) { // подсчитываем, сколько байт сжато num_bytes = data-192; // читаем байт цвета data = getc(fp); // повторяем байты в буфере num_bytes раз while(num_bytes-->0) { image->buffer[count++] = data; } // конец цикла while } // конец оператора if else { // помещаем данные в следующую позицию буфера image->buffer[count++] = data; } // конец оператора else } // конец чтения байт изображения // перейти в позицию, не доходя 768 байт от конца файла fseek(fp,-768L,SEEK_END); // читаем палитру и загружаем ее в регистры VGA for (index=0; index<256; index++) { // красная составляющая image->palette[index].red = (getc(fp) >> 2); // зеленая составляющая image->palette[index].green = (getc(fp) >> 2); // синяя составляющая image->palette[index].blue = (getc(fp) >> 2); } // конец цикла for - 144 - fclose(fp); // если флаг enable_palette установлен, меняем палитру // на загруженную из файла if (enable_palette) { for (index=0; index<256; index++) { Set_Palette_Register(index, (RGB_color_ptr)&image->palette[index]); } // конец цикла for } // конец установки новой палитры } // конец функции __________________________________________________________________________ Функция PCX_Load() - это сердце всей программы. Она загружает PCX-файл, декодирует его в буфере и загружает палитру. Каждый PCX-файл имеет свою собственную палитру в конце файла, и я думаю, что вы сами можете добавить возможность загрузки новой палитры в таблицу соответствия цветов. Функция выполняет именно те действия, которые мы с вами уже обсуждали и ничего больше: - Открывает PCX-файл; - Читает заголовок; - Загружает PCX-файл и декомпресситует все 64000 пикселей; - загружает цветовую палитру. В общем, все это несложно. А вот что делать с картинками, которые больше, чем целый экран? Ответ прост: можно декодировать только маленький кусочек, скажем 24 на 24 пикселя. Я создал для вас заготовку CHARPLATE.PCX, которую вы найдете на прилагаемом диске. Если вы посмотрите на него, то увидите множество маленьких белых квадратов. Вы можете использовать этот шаблон для рисования ваших игровых персонажей в этих квадратиках. Кстати, впоследствии мы научимся извлекать битовые образы из больших PCX-файлов и использовать их в качестве персонажей игры. Возникает вопрос: "Как редактировать PCX-файлы в режиме 320х200х256?" Для этого можно воспользоваться такими условно-бесплатными программами как VGA-Paint или Pro-Paint. Тем не менее, я надеюсь, что самые - 145 - расторопные читатели уже давно пользуются копией Electronic Art's Deluxe Paint & Animation. Это одна из самых классных программ для рисования на ПК. Она корректно работает с режимом 320х200х256 и имеет множество полезных функций для преобразования и анимации изображения. ПОБИТОВОЕ КОПИРОВАНИЕ ИЗОБРАЖЕНИЯ (БИТ-БЛИТТИНГ) Термин бит-блиттинг (bit blitting) - означает процесс перемещения группы битов (образа) из одного места экрана в другое. В играх на ПК нас интересует перемешение образа из области хранения вне экрана в область видеобуфера. Давайте посмотрим на рис.5.9, чтобы уяснить сущность этой операции. Рис.5.9. Копирование битового образа (бит-блиттинг) в действии. Как вы можете видеть, матрица пикселей обычно копируется один к одному из исходной области хранения в область буфера экрана. Кроме того, еще встречаются ситуации, когда что-то из видеобуфера копируется в память для дальнейшего использования. Чтобы понять суть перемещения, нам следовало бы написать несколько функций, которые бы брали битовую карту из PCX-файла и перемещали ее на экран. Но я хочу проявить некоторую "авторскую вольность" и поговорить о спрайтах и их анимации. СПРАЙТЫ Вы можете спросить: "Что такое спрайт?". Знаете, есть такой газированный напиток... Снова шучу. На самом деле спрайты - это маленькие объектики, которые находятся на игровом поле и могут двигаться. Этот термин - 146 - прижился с легкой руки программистов фирмы Atari и Apple в середине 70-х годов. Теперь поговорим о спрайтах и их анимации. В будущем мы еще вернемся к этой теме в 7-й главе "Продвинутая битовая графика и специальные эффекты". Именно с этой мыслью я создал несколько небольших спрайтов, которые мы будем использовать в дальнейшем. Спрайты - это персонажи в играх для ПК, которые могут без труда перемещаться по экрану, изменять цвет и размер. Все это звучит как мечта программиста. Но надо помнить, что в IBM-совместимых компьютерах нет спрайтов! В нормальных компьютерах существует аппаратная поддержка спрайтов. Такие машины как Atari, Amiga, Commodore и последние модели Apple имеют эту возможность, а вот ПК - нет. Поэтому мы вынуждены делать это самостоятельно. М-да. Нам будет чем заняться. Конечно, мы не станем заниматься разработкой аппаратной поддержки спрайтов. Все, что нам нужно, это понять, каким образом помещать образ на экран, сохраняя при этом возможность его перемещений и видоизменений. Поскольку спрайт - это довольно сложный объект, то стоит подумать о том, как это реализовать на программном уровне. Мы вовремя об этом заговорили: вспомните разработку игры "Астероиды". Конечно, мы не будем заниматься разработкой аппаратной поддержки спрайтов. Все, что нам нужно, это понять, каким образом помещать образ на экран, сохраняя при этом возможность его перемещений и видоизменений. Поскольку спрайт - это довольно сложный объект, то стоит подумать о том, как это реализовать на программном уровне. Мы вовремя об этом заговорили: вспомните разработку игры "Астероиды". Вот что нам надо: - Мы должны уметь извлекать матрицу пикселей из загруженного PCX-образа и сохранять ее в буфере, связанном со спрайтом; - Более того, хотелось бы считывать сразу несколько образов из PCX-файла и загружать их в массив, связанный с одним спрайтом. Это позволит нам оптимизировать программу по скорости выполнения. Рис.5.10 показывает последовательность кадров, которые приводят в движение ковбоя. Мы воспользуемся ею позже. Рис.5.10. Идущий ковбой. После того, как мы загрузим данные из PCX-файла, нам необходимо иметь возможность показывать спрайт в любой позиции на экране. Делать это нужно осторожно, поскольку запись пикселей в видеобуфер разрушает то, что было на их месте. Поэтому мы должны уметь сохранять ту часть изображения, которая окажется закрыта спрайтом, чтобы в дальнейшем иметь возможность восстановить первоначальный вид экрана. - 147 - Давайте на этом месте остановимся и поговорим чуть-чуть об анимации. В играх для ПК применяется 2 способа обновления экрана. - Мы можем перерисовывать весь экран целиком, как это сделано в игре Wolfenstein 3D; - Можно перерисовывать лишь участки экрана. Какой из способов лучше выбрать, зависит от типа игры. Если мы перерисовываем весь экран, то это нужно делать по возможности быстро, поскольку 64000 пикселей - все же довольно много. Если мы перерисовываем только участки экрана, то желательно быть уверенным, что фон после прохождения спрайта не изменится. Поскольку все игры для ПК отличаются друг от друга, то для решения конкретных специфических задач всегда надо выбирать наиболее подходящую технику. Давайте рассмотрим способ, которым мы будем пользоваться в настоящей главе - это перерисовка участков экрана. Посмотрим на рис.5.11, чтобы представить последовательность событий, позволяющих спрайту правильно перемещаться по экрану. Рис.5.11. Фазы анимации. Теперь, когда мы знаем что делать, надо попробовать это реализовать. Для начала создадим структуру данных спрайта. Листинг 5.11 содержит необходимый для этого код. Листинг 5.11. Структура спрайта с полями для анимации. -------------------------------------------------------------------------- typedef struct sprite_typ { int x,y; // текущая позиция спрайта int x_old,y_old; // предыдущая позиция спрайта int width,height; // размеры спрайта int anim_clock; // время анимации int anim_speed; // скорость анимации int motion_speed; // скорость движения int motion_clock // время движения char far *frames[MAX_SPRITE_FRAMES]; // массив указателей // на кадры - 148 - int curr_frame; // отображаемый кадр int num_frames; // общее число кадров int state; // статус спрайта char far &background; // фон под спрайтом } sprite, *sprite_ptr; __________________________________________________________________________ Структура спрайта имеет поля для сохранения позиции и размеров образа и несколько других элементов. Сейчас мы уже готовы написать функцию для работы со спрайтами. Прежде всего мы должны извлечь битовую карту из PCX-файла и поместить ее в массив, хранящий образы спрайта. Если вы помните, я создал файл в формате PCX (CHARPLATE.PCX), в который вы можете дорисовать свои картинки и героев. Функция, извлекающая битовые карты из PCX-образа подразумевает, что вы создали свои образы с помощью этого файла. Программа из Листинга 5.12 позволяет перемещать спрайт, который вы хотите изменить, в указанные координаты. Листинг 5.12. Функция извлечения спрайта из загруженного PCX-файла. -------------------------------------------------------------------------- void PCX_Grap_Bitmap(pcx_picture_ptr image, sprite_ptr sprite, int sprite_frame, int grab_x, int grab_y) { // функция выделяет одно изображение из буфера, в который // загружен PCX-файл // функция исходит из предположения, что в действительности массив // пикселей размером 320х200 разделен на отдельные изображения // размером 24х24 пикселя int x_off,y_off, x,y, index; char far *sprite_data; // вначале выделяем память для хранения спрайта в структуре спрайта sprite->frames[sprite_frame] = (char far *)malloc(SPRITE_WIDTH * SPRITE_HEIGHT); // создаем альтернативный указатель на эту область памяти // для ускорения доступа sprite_data = sprite->frames[sprite_frame]; // теперь перемещаем битовый образ спрайта из области PCX-файла // в выделенную память // мы должны выбрать, какой именно спрайт мы копируем - 149 - // помните, что в действительности файл представляет собой массив // 12х8 элементов, каждый из которых имеет размер 24х24 пикселя. // Индекс (0,0) соответствует верхнему левому углу спрайта, // (11,7) - нижнему правому x_off = 25 * grab_x + 1; y_off = 25 * grab_y + 1; // вычисляем начальный адрес y_off = y_off * 320; for (y=0; ybuffer[y_off + x_off + x]; } // конец копирования строки // перейти к следующей строке y_off+=320; } // конец копирования // инкрементировать счетчик кадров sprite->num_frames++; } // конец функции ___________________________________________________________________________ Эта функция по указателю на спрайт определяет его расположение в загруженном файле. Далее она выделяет память для хранения образа и инициализирует структуру данных (я решил делать спрайты размером 24 на 24 пикселя, но вам ничто не мешает изготавливать любые другие спрайты). Теперь, когда у нас подготовлены образы, следующим шагом будет их отображение на экране монитора. Для этого нам надо: - Вычислить начальную позицию спрайта согласно его координатам (x,y); - Преобразовать полученные координаты в адрес видеобуфера; - Переместить байты изображения в видеобуфер. Для рисования спрайта мы должны выполнить все операции с текущим кадром анимации. Код в Листинге 5.13 делает все перечисленное. - 150 - Листинг 5.13. Рисование спрайта. -------------------------------------------------------------------------- viod Draw_Sprite(sprite_ptr sprite) { // функция, рисующая спрайт на экране строка за строкой, // очень быстро. Вместо умножения используется сдвиг char far *work_sprite; int work_offset=0,offset,x,y; unsigned char data; // создаем альтернативный указатель на спрайт для ускорения доступа work_sprite = sprite->frames[sprite->curr_frame]; // вычислить смещение спрайта в видеобуфере offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x; for (y=0; ybackground; // вычисляем смещение в видеобуфере offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x; - 152 - for (y=0; y>1); } // конец обработки строки // переходим к следующей строке offset += SCREEN_WIDTH; work_char++; } // конец рисования символа } // конец функции -------------------------------------------------------------------------- - 155 - Теперь и печать текстов не представляет труда. Нужно только выдерживать пространство между символами и печатать их из заданной строки один за другим, пока не встретится конец строки (NULL). В демонстрационной программе в конце этой главы мы используем подобную функцию. Теперь давайте поговорим о методах анимации и рендеринга, которые применяются в технике программирования игр. ДУБЛИРУЮЩЕЕ БУФЕРИЗИРОВАНИЕ Дублирующее буферизирование [в книге используется термин "дублирующий буфер", но в отечественной литературе это же понятие чаще обозначается словами "виртуальный" или "теневой экран" (прим.ред.)] - это метод, позволяющий избежать мерцания, которое может возникать при перемещении объектов. Когда функции, которые мы пишем, рисуют спрайты на экране, то они это делают, не учитывая статус состояния VGA-карты, т.е. в них отсутствует синхронизация с дисплеем. Существует 2 способа уменьшить возможное мигание. Первый метод называется дублирующим буферизированием. При этом экранное изображение формируется в памяти, а затем копируется в видеобуфер. Это позволяет минимизировать перемещение маленьких участков в видеопамяти, поскольку сразу перемещается один блок. Пример дублирующего буферизирования изображен на рис.15.4. Переключение страниц - это видоизмененное дублирующее буферизирование. При использовании этого метода две видеостраницы находятся во взаимосвязи. Когда одна из них воспроизводится на экране, другая перерисовывается. Рис.5.14. Дублирующее буферизирование. - 156 - Новая страница затем воспроизводится переключением указателя на нее. Обратите внимание на рис. 5.15 для осмысления этого метода. Рис.5.15. Переключение страниц. Оба этих метода имеют общий недостаток - уменьшение быстродействия в 2 раза, но при этом отсутствует мигание и достигается очень высокое качество анимации. Мы обратимся к этому методу еще раз в 7-й главе "Продвинутая битовая графика и специальные эффекты". Сейчас поговорим об уменьшении мерцания. ВЕРТИКАЛЬНЫЙ ОБРАТНЫЙ ХОД ЛУЧА Образ, рисуемый на экране ЭЛТ (электронно-лучевой трубки) и управляемый картой VGA, образуется в результате взаимодействия следующих факторов: - Луч электронов движется по экрану слева направо и сверху вниз, рисуя картинку; - Когда он достигает нижней границы, он вновь возвращается вверх и все начинается сначала. Рис.5.16 показывает это. - 157 - Рис.5.16. Вертикальный обратный ход луча. - Чтобы вернуться в исходную позицию, лучу требуется примерно 1/60 секунды. Это идеальное время для обновления видеобуфера. В течение этого периода видеобуфер недоступен VGA-карте. Таким образом, 1/60 секунды - это аппаратно-зависимый параметр. В 7-й главе "Продвинутая битовая графика и специальные эффекты" мы узнаем, как "синхронизировать" наши игры с этим сигналом и создать чистые, свободные от мерцания изображения. ТАЙМИНГ Я попытался придумать хорошую демонстрацию перемещения спрайтов и решил, что для этого подойдет маленький городок с ковбоем, который ходит по улице. Не так уж плохо. Я хотел еще, чтобы он время от времени стрелял, но позже решил не усложнять дело. Для осуществления реалистичной анимации мы должны уделять большое внимание таймингу, то есть задержке между выводом кадров. Если образ имеет 10 анимационных кадров и мы будем их менять слишком быстро, то персонаж станет похож на лунатика. Поэтому мы должны иметь в программе счетчики времени, чтобы наши персонажи выполняли определенные действия с заданной скоростью. В нашем случае мы используем 4 переменные для сохранения счетчиков движения и анимации: - anim_clock - anim_speed - motion_clock - motion_speed Переменные скорости - это константы, а переменные времени обновляются при каждом проходе через главный цикл. Когда переменная времени оказывается - 158 - больше переменной скорости, то мы осуществляем какое-либо действие: перемещаем спрайт или сменяем кадр. Переменные времени при этом обнуляются. Это позволяет нам сделать перемещение спрайта по экрану и смену фаз движения независимым друг от друга и от быстродействия машины. Мы еще вернемся к этому в 7-й главе. ИГРА TOMBSTONE Мы начали с того, что вообще ничего не знали о VGA-карте. Сейчас мы знаем очень много. Поэтому я думаю, что было бы лучше, если бы вы написали маленькую двухмерную игру для закрепления пройденного материала.Чтобы вам в этом помочь, я написал демонстрацию и назвал ее Tombstone. В этой демонстрации маленький ковбой ходит по городу с различной скоростью. У вас есть все инструменты, чтобы "дать" ему пистолет и "научить" стрелять. В PCX-файле на дискете вы найдете для этого все необходимые картинки. Вся программа за исключением функции Set_Mode() дана в Листинге 5.16. Прилинкуйте Set_Mode(), когда будете создавать исполняемый файл. Листинг 5.16. Tombstone (TOMB.C). __________________________________________________________________________ // I N C L U D E S /////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include // D E F I N E S //////////////////////////////////////////////////////////// #define ROM_CHAR_SET_SEG 0xF000 // segment of 8x8 ROM character set #define ROM_CHAR_SET_OFF 0xFA6E // begining offset of 8x8 ROM character set #define VGA256 0x13 #define TEXT_MODE 0x03 #define PALETTE_MASK 0x3c6 #define PALETTE_REGISTER_RD 0x3c7 #define PALETTE_REGISTER_WR 0x3c8 #define PALETTE_DATA 0x3c9 #define SCREEN_WIDTH (unsigned int)320 #define SCREEN_HEIGHT (unsigned int)200 #define CHAR_WIDTH 8 #define CHAR_HEIGHT 8 #define SPRITE_WIDTH 24 #define SPRITE_HEIGHT 24 #define MAX_SPRITE_FRAMES 16 #define SPRITE_DEAD 0 #define SPRITE_ALIVE 1 #define SPRITE_DYING 2 // S T R U C T U R E S /////////////////////////////////////////////////////// // this structure holds a RGB triple in three bytes typedef struct RGB_color_typ { unsigned char red; // red component of color 0-63 unsigned char green; // green component of color 0-63 unsigned char blue; // blue component of color 0-63 } RGB_color, *RGB_color_ptr; typedef struct pcx_header_typ { char manufacturer; char version; char encoding; char bits_per_pixel; int x,y; int width,height; int horz_res; int vert_res; char ega_palette[48]; char reserved; char num_color_planes; int bytes_per_line; int palette_type; char padding[58]; } pcx_header, *pcx_header_ptr; typedef struct pcx_picture_typ { pcx_header header; RGB_color palette[256]; char far *buffer; } pcx_picture, *pcx_picture_ptr; typedef struct sprite_typ { int x,y; // position of sprite int x_old,y_old; // old position of sprite int width,height; // dimensions of sprite in pixels int anim_clock; // the animation clock int anim_speed; // the animation speed int motion_speed; // the motion speed int motion_clock; // the motion clock char far *frames[MAX_SPRITE_FRAMES]; // array of pointers to the images int curr_frame; // current frame being displayed int num_frames; // total number of frames int state; // state of sprite, alive, dead... char far *background; // whats under the sprite } sprite, *sprite_ptr; // E X T E R N A L S ///////////////////////////////////////////////////////// extern Set_Mode(int mode); // P R O T O T Y P E S /////////////////////////////////////////////////////// void Set_Palette_Register(int index, RGB_color_ptr color); void Plot_Pixel_Fast(int x,int y,unsigned char color); void PCX_Init(pcx_picture *image); void PCX_Delete(pcx_picture *image); void PCX_Load(char *filename, pcx_picture_ptr image,int enable_palette); void PCX_Show_Buffer(pcx_picture_ptr image); // G L O B A L S //////////////////////////////////////////////////////////// unsigned char far *video_buffer = (char far *)0xA0000000L; // vram byte ptr unsigned int far *video_buffer_w= (int far *)0xA0000000L; // vram word ptr unsigned char far *rom_char_set = (char far *)0xF000FA6EL; // rom characters 8x8 // F U N C T I O N S ///////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void Blit_Char(int xc,int yc,char c,int color) { // this function uses the rom 8x8 character set to blit a character on the // video screen, notice the trick used to extract bits out of each character // byte that comprises a line int offset,x,y; unsigned char data; char far *work_char; unsigned char bit_mask = 0x80; // compute starting offset in rom character lookup table work_char = rom_char_set + c * CHAR_HEIGHT; // compute offset of character in video buffer offset = (yc << 8) + (yc << 6) + xc; for (y=0; y>1); } // end for x // move to next line in video buffer and in rom character data area offset += SCREEN_WIDTH; work_char++; } // end for y } // end Blit_Char ////////////////////////////////////////////////////////////////////////////// void Blit_String(int x,int y,int color, char *string) { // this function blits an entire string on the screen with fixed spacing // between each character. it calls blit_char. int index; for (index=0; string[index]!=0; index++) { Blit_Char(x+(index<<3),y,string[index],color); } /* end while */ } /* end Blit_String */ ////////////////////////////////////////////////////////////////////////////// void Delay(int t) { float x = 1; while(t-->0) x=cos(x); } // end Delay ////////////////////////////////////////////////////////////////////////////// void Set_Palette_Register(int index, RGB_color_ptr color) { // this function sets a single color look up table value indexed by index // with the value in the color structure // tell VGA card we are going to update a pallete register _outp(PALETTE_MASK,0xff); // tell vga card which register we will be updating _outp(PALETTE_REGISTER_WR, index); // now update the RGB triple, note the same port is used each time _outp(PALETTE_DATA,color->red); _outp(PALETTE_DATA,color->green); _outp(PALETTE_DATA,color->blue); } // end Set_Palette_Color ////////////////////////////////////////////////////////////////////////////// void PCX_Init(pcx_picture_ptr image) { // this function allocates the buffer region needed to load a pcx file if (!(image->buffer = (char far *)malloc(SCREEN_WIDTH * SCREEN_HEIGHT + 1))) printf("\ncouldn't allocate screen buffer"); } // end PCX_Init ////////////////////////////////////////////////////////////////////////////// void Plot_Pixel_Fast(int x,int y,unsigned char color) { // plots the pixel in the desired color a little quicker using binary shifting // to accomplish the multiplications // use the fact that 320*y = 256*y + 64*y = y<<8 + y<<6 video_buffer[((y<<8) + (y<<6)) + x] = color; } // end Plot_Pixel_Fast ////////////////////////////////////////////////////////////////////////////// void PCX_Delete(pcx_picture_ptr image) { // this function de-allocates the buffer region used for the pcx file load _ffree(image->buffer); } // end PCX_Delete ////////////////////////////////////////////////////////////////////////////// void PCX_Load(char *filename, pcx_picture_ptr image,int enable_palette) { // this function loads a pcx file into a picture structure, the actual image // data for the pcx file is decompressed and expanded into a secondary buffer // within the picture structure, the separate images can be grabbed from this // buffer later. also the header and palette are loaded FILE *fp, *fopen(); int num_bytes,index; long count; unsigned char data; char far *temp_buffer; // open the file fp = fopen(filename,"rb"); // load the header temp_buffer = (char far *)image; for (index=0; index<128; index++) { temp_buffer[index] = getc(fp); } // end for index // load the data and decompress into buffer count=0; while(count<=SCREEN_WIDTH * SCREEN_HEIGHT) { // get the first piece of data data = getc(fp); // is this a rle? if (data>=192 && data<=255) { // how many bytes in run? num_bytes = data-192; // get the actual data for the run data = getc(fp); // replicate data in buffer num_bytes times while(num_bytes-->0) { image->buffer[count++] = data; } // end while } // end if rle else { // actual data, just copy it into buffer at next location image->buffer[count++] = data; } // end else not rle } // end while // move to end of file then back up 768 bytes i.e. to begining of palette fseek(fp,-768L,SEEK_END); // load the pallete into the palette for (index=0; index<256; index++) { // get the red component image->palette[index].red = (getc(fp) >> 2); // get the green component image->palette[index].green = (getc(fp) >> 2); // get the blue component image->palette[index].blue = (getc(fp) >> 2); } // end for index fclose(fp); // change the palette to newly loaded palette if commanded to do so if (enable_palette) { for (index=0; index<256; index++) { Set_Palette_Register(index,(RGB_color_ptr)&image->palette[index]); } // end for index } // end if change palette } // end PCX_Load ////////////////////////////////////////////////////////////////////////////// void PCX_Show_Buffer(pcx_picture_ptr image) { // just copy he pcx buffer into the video buffer _fmemcpy((char far *)video_buffer, (char far *)image->buffer,SCREEN_WIDTH*SCREEN_HEIGHT); } // end PCX_Show_Picture ////////////////////////////////////////////////////////////////////////////// void Sprite_Init(sprite_ptr sprite,int x,int y,int ac,int as,int mc,int ms) { // this function initializes a sprite with the sent data int index; sprite->x = x; sprite->y = y; sprite->x_old = x; sprite->y_old = y; sprite->width = SPRITE_WIDTH; sprite->height = SPRITE_HEIGHT; sprite->anim_clock = ac; sprite->anim_speed = as; sprite->motion_clock = mc; sprite->motion_speed = ms; sprite->curr_frame = 0; sprite->state = SPRITE_DEAD; sprite->num_frames = 0; sprite->background = (char far *)malloc(SPRITE_WIDTH * SPRITE_HEIGHT+1); // set all bitmap pointers to null for (index=0; indexframes[index] = NULL; } // end Sprite_Init ////////////////////////////////////////////////////////////////////////////// void Sprite_Delete(sprite_ptr sprite) { // this function deletes all the memory associated with a sprire int index; _ffree(sprite->background); // now de-allocate all the animation frames for (index=0; indexframes[index]); } // end Sprite_Delete ////////////////////////////////////////////////////////////////////////////// void PCX_Grap_Bitmap(pcx_picture_ptr image, sprite_ptr sprite, int sprite_frame, int grab_x, int grab_y) { // this function will grap a bitmap from the pcx frame buffer. it uses the // convention that the 320x200 pixel matrix is sub divided into a smaller // matrix of 12x8 adjacent squares each being a 24x24 pixel bitmap // the caller sends the pcx picture along with the sprite to save the image // into and the frame of the sprite. finally, the position of the bitmap // that should be grabbed is sent int x_off,y_off, x,y, index; char far *sprite_data; // first allocate the memory for the sprite in the sprite structure sprite->frames[sprite_frame] = (char far *)malloc(SPRITE_WIDTH * SPRITE_HEIGHT); // create an alias to the sprite frame for ease of access sprite_data = sprite->frames[sprite_frame]; // now load the sprite data into the sprite frame array from the pcx picture // we need to find which bitmap to scan, remember the pcx picture is really a // 12x8 matrix of bitmaps where each bitmap is 24x24 pixels. note:0,0 is upper // left bitmap and 11,7 is the lower right bitmap. x_off = 25 * grab_x + 1; y_off = 25 * grab_y + 1; // compute starting y address y_off = y_off * 320; for (y=0; ybuffer[y_off + x_off + x]; } // end for x // move to next line of picture buffer y_off+=320; } // end for y // increment number of frames sprite->num_frames++; // done!, let's bail! } // end PCX_Grap_Bitmap ////////////////////////////////////////////////////////////////////////////// void Behind_Sprite(sprite_ptr sprite) { // this function scans the background behind a sprite so that when the sprite // is draw, the background isnn'y obliterated char far *work_back; int work_offset=0,offset,y; // alias a pointer to sprite background for ease of access work_back = sprite->background; // compute offset of background in video buffer offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x; for (y=0; ybackground; // compute offset of background in video buffer offset = (sprite->y_old << 8) + (sprite->y_old << 6) + sprite->x_old; for (y=0; yframes[sprite->curr_frame]; // compute offset of sprite in video buffer offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x; for (y=0; y cowboy.anim_speed) { // reset the animation clock cowboy.anim_clock = 0; if (++cowboy.curr_frame >= cowboy.num_frames) { cowboy.curr_frame = 0; } // end if reached last frame redraw=1; } // end if time to change frames // now test if its time to move the cowboy if (++cowboy.motion_clock > cowboy.motion_speed) { // reset the motion clock cowboy.motion_clock = 0; // save old position cowboy.x_old = cowboy.x; redraw = 1; // move cowboy if (++cowboy.x >= SCREEN_WIDTH-2*SPRITE_WIDTH) { Erase_Sprite((sprite_ptr)&cowboy); cowboy.state = SPRITE_DEAD; redraw = 0; } // end if reached last frame } // end if time to change frames } // end if cowboy alive else { // try and start up another cowboy if (rand()%100 == 0 ) { cowboy.state = SPRITE_ALIVE; cowboy.x = SPRITE_WIDTH; cowboy.curr_frame = 0; cowboy.anim_speed = 3 + rand()%6; cowboy.motion_speed = 1 + rand()%3; cowboy.anim_clock = 0; cowboy.motion_clock = 0; Behind_Sprite((sprite_ptr)&cowboy); } } // end else dead, try to bring back to life // now the sprite has had it's state updated if (redraw) { // erase sprite at old position Erase_Sprite((sprite_ptr)&cowboy); // scan the background at new postition Behind_Sprite((sprite_ptr)&cowboy); // draw sprite at new position Draw_Sprite((sprite_ptr)&cowboy); // update old position cowboy.x_old = cowboy.x; cowboy.y_old = cowboy.y; } // end if sprites needed to be redrawn Delay(1000); } // end while // make a cool clear screen, disolve screen, in one line, eye might add! for (index=0; index<=300000; index++,Plot_Pixel_Fast(rand()%320, rand()%200, 0)); // go back to text mode Set_Mode(TEXT_MODE); } // end main __________________________________________________________________________ - 172 - ИТОГ Эта глава, наверное, самая важная в книге. Среди всей информации, которая, может быть, не относится напрямую к играм, вы узнали общую концепцию построения видеоигр, что абсолютно необходимо для их написания. Так что, если вы чего-то не поняли, то остановитесь и прочтите главу вновь (не беспокойтесь, я вас подожду). Если вы прочитали и поняли в этой главе все, то должны знать, что: - Мы изучили VGA-карту и ее архитектуру; - Мы узнали о режиме 13h, который дает лучшее разрешение и наиболее прост для программирования; - Мы узнали, как в режиме 13h программировать регистры цвета, рисовать пиксели, загружать PCX-файлы и перемещать битовые образы; - Мы поговорили о спрайтах и создали простую библиотеку для работы с ними; - Мы затронули довольно сложные вещи, такие как дублирующий буфер и вертикальная синхронизация, о которых более подробно мы поговорим чуть позже; - И, наконец, собрав все вместе, мы сделали демонстрационную программу, рисующую маленького ковбоя, который ходит по городу. До встречи в новой главе.